/**
 * @file   StreamFile.java
 * @author Enrique Madrid <e.madrid@lpcsite.com>
 * @date   Wed Jul 25 14:39:33 2007
 *
 *   Archivos de acceso aleatorio utilizando cache configurable.
 *
 *
 */

package sim.framework.io;

import sim.framework.Simlet;
import sim.utils.Utils;

/**
 *  Archivos de acceso aleatorio utilizando cache configurable.
 *
 * Esta clase permite la manipulación de archivos para acceso aleatorio
 * utilizando un cache intermedio.
 *
 * @author Enrique J. Madrid M.
 * @version %I%, %I%
 * @since 1.0
 */
public class FileStream extends File {
        
    private static final int CACHE_BESTLEN = 4*File.RECORD_MAXLEN;
    
    private byte[] m_Cache;	// Memoria Cache Cache Cache ... ;-)
    private int m_CacheOffset;	// Puntero en el archivo del primer byte de la memoria cache.
    private int m_CacheFinish;	// Ultimo byte valido de la memoria cache.
    private int m_CacheModify;	// Posición del primer byte modificado de la memoria cache. (>= 0 && < m_CacheFinish).
    
    private byte[]  m_Data;
    private int     m_DataLen;
    private int     m_LineEnd;    
    
    /**
     *  Construye un objeto que perminte el acceso a archivo utilizando cache especificado.
     *
     * @param s Refencia a un objeto del tipo Simlet.
     * @param cache Longitud del buffer intermedio utilizado.
     *
     * @return Devuelve un referencia a un objeto del tipo FileStream.
     */
    public FileStream(Simlet s, int cache) {
        super(s);
        lockedForCache(cache);
        m_Data = new byte[cache/4];
    }
    
    /**
     *  Construye un objeto que permite el acceso a un archivo utilizando un cache optimizado.
     *
     * @param s Refencia a un objeto del tipo Simlet.
     *
     * @return Devuelve un referencia a un objeto del tipo FileStream.
     */
    public FileStream(Simlet s) {
        this(s, FileStream.CACHE_BESTLEN);
    }
    
    /**
     *  Abre una archivo utilizando un cache especificado.
     *
     * Uso :
     *
     * FileStream fs = (FileStream)FileStream.create(...);
     *
     * @param s Referencia a un objeto del tipo Simlet.
     * @param filename Nombre del archivo.
     * @param mode Modo apertura. Ver File.MODE_XXX
     * @param cache Longitud del buffer intermedio utilizado.
     *
     * @return Devuelve una referencia a un objeto del tipo FileStream
     *         apuntado por una referencia de la clase Io.
     */
    public static Io create(Simlet s, final String filename, byte mode, int cache) {
        FileStream fs;
        fs = new FileStream(s, cache);
        if (!fs.open(filename, mode)) {
            fs = null;
        }
        return fs;
    }
    
    /**
     *  Abre una archivo utilizando un cache optimizado.
     *
     * Uso :
     *
     * FileStream fs = (FileStream)FileStream.create(...);
     *
     * @param s Referencia a un objeto del tipo Simlet.
     * @param filename Nombre del archivo.
     * @param mode Modo apertura. Ver File.MODE_XXX
     *
     * @return Devuelve una referencia a un objeto del tipo FileStream
     *         apuntado por una referencia de la clase Io.
     */
    public static Io create(Simlet s, final String filename, byte mode) {
        return FileStream.create(s, filename, mode, FileStream.CACHE_BESTLEN);
    }
    
    
    /**
     *  Abre una archivo en modo File.MODE_WRITEEX utilizando un cache optimizado.
     *
     * Uso :
     *
     * FileStream fs = (FileStream)FileStream.create(...);
     *
     * @param s Referencia a un objeto del tipo Simlet.
     * @param filename Nombre del archivo.
     *
     * @return Devuelve una referencia a un objeto del tipo FileStream
     *         apuntado por una referencia de la clase Io.
     */
    public static Io create(Simlet s, final String filename) {
        return FileStream.create(s, filename, File.MODE_WRITEEX);
    }
    
    /**
     * La siguiente función es el core del algoritmo.
     *
     * Esta función implementa el algoritmo de paginación. Este algoritmo
     * decide cuando cargar o gurdar información de disco.
     * Esta función es llamada por operaciones de lectura y escritura
     * para que separe espacio y calcule la posición en que debe leer
     * escribir desde o hacia la memoria cache.
     */
    private int seekCache(byte mode, int count) {
        int ret;
        int cachePos;
                
        // Recuerde: m_Offset es una variable que se hereda de la clase File y es
        // un valor entre m_CacheOffset y (m_CacheOffset + m_CacheFinish), sino...
        // Dont worry be hacker!!!.
        
        ret = 0;
        switch(mode) {
	case File.MODE_READ:
	    // Calcular la max. longitud en bytes que podemos leer de cache.
	    cachePos = m_Offset - m_CacheOffset;
	    ret = m_CacheFinish - cachePos;
	    // Si no hay nada que leer...
	    if (ret == 0) {
		// Volcar todo lo modificado ...
		if (lockedForFlush() == 0) {
		    // Cargar a cache un sector del disco ...
		    if (learn()) {
			// Devolver la longitud en bytes que hemos memorizado.
			ret = m_CacheFinish;
		    }
		}
	    }
	    break;
	case File.MODE_WRITE:
	    // Calcular la max. longitud en bytes que podemos escribir en cache
	    cachePos = m_Offset - m_CacheOffset;
	    ret = m_Cache.length - cachePos;
	    // Si no hay espacio en cache
	    if (ret == 0) {
		// ... volcar lo modificado a disco y hacer máximo espacio,
		// Si volcamos todo lo modificado a disco ...
		if (lockedForFlush() == 0) {
		    // ... Mover m_Offset al principio del cache
		    Utils.memmove(m_Cache, cachePos, 0, m_Cache.length - cachePos);
		    m_CacheOffset = getOffset();
		    // La Posción del último byte modificado ahora es:
		    m_CacheFinish -= cachePos;
		    // El cache disponible es:
		    ret = m_Cache.length;
		}
	    }
        }
                
        return ret;
    }
    
    private boolean learn() {
        int cacheFinish;
        int offset;
        // Posición actual en el archivo.
        offset = getOffset();
        // Leer todo lo que cache soporte
        cacheFinish = super.lockedForRead(m_Cache, 0, m_Cache.length);
        // Si se leyeron algunos bytes, entonces :
        if (cacheFinish > 0) {
            // - Se tiene en cache a partir de
            m_CacheOffset = offset;
            // - Se puede leer a partir de
            m_Offset = offset;
            // - Lo máximo que se puede leer es
            m_CacheFinish = cacheFinish;
            return true;
        }
        return false;
    }
    
    protected synchronized int lock(byte operation, final String filename, byte mode,
				    byte[] buffer, int start, int count) {
	int ret = 0;
        switch (operation) {
	case File.OPER_OPEN:
	    ret = lockedFor(operation, filename, mode, buffer, start, count);
	    break;
	default:
	    // Si el store esta abierto ...
	    if (opened()) {
		// enviamos el mensaje!
		ret = lockedFor(operation, filename, mode, buffer, start, count);
	    }
	}
	return ret;
    }
    
    protected int lockedFor(byte operation, final String filename, byte mode,
			    byte[] buffer, int start, int count) {
        int ret;
        
        ret = File.ERR;
        switch(operation) {
	case FileStream.OPER_CACHE:
	    ret = lockedForCache(count);
	    break;
	case FileStream.OPER_FLUSH:
	    ret = lockedForFlush();
	    break;
	default:
	    ret = super.lockedFor(operation, filename, mode, buffer, start, count);
        }
                
        return ret;
    }
    
    protected int lockedForCache(int length) {
        // Nos piden redimencionar el cache, :-|
        if (lockedForFlush() == 0) {
            m_Cache = null;
            m_Cache = new byte[length];
            // Paranoia :-S
            if (m_Cache != null) {
                m_CacheFinish = 0;	// No se tiene ningun byte en memoria cache.
                m_CacheModify = -1;	// No se ha modificado ningun byte de la memoria cache.
                // Y si se preguntan por m_CacheOffset ... ese sigue apuntando a m_Offset. :-)
                // Dont worry be hacker!!!.
                return 0;
            }
        }
        return File.ERR;
    }
    
    protected int lockedForWrite(final byte[] bufIn, int start, int count) {
        int maxWrite;		// Máxima longitud de bytes a escribir.
        int ret;		// Bytes escritos.
        int maxCopy;		// Max. longitud de bytes a copiar hacia cache.
        
        ret = 0;
        maxWrite = Math.min(count, bufIn.length - start);
        while (ret < maxWrite) {
            // Separar cache para escribir (maxWrite - ret)
            maxCopy = seekCache(File.MODE_WRITE, maxWrite - ret);
            // Del cache disponible, utilizar lo necesario.
            maxCopy = Math.min(maxCopy, maxWrite - ret);
            // Si no hay cache o no se desea escribir, abandonar.
            if (maxCopy == 0) {
                if (err()) {
                    ret = File.ERR;
                }
                break;
            }
            // Establecer el primer byte modificado de la memoria cache
            if (m_CacheModify < 0) {
                m_CacheModify = m_Offset - m_CacheOffset;
            }
            // Escribir en cache, y e incrementar el valor retornado
            Utils.memmove(m_Cache, m_Offset - m_CacheOffset, // destino
			  bufIn, start + ret, // origen
			  maxCopy);
            ret += maxCopy;
            // Mover el offset.
            m_Offset += maxCopy;
            // Ultimo byte valido de la memoria cache
            if (m_Offset - m_CacheOffset > m_CacheFinish) {
                m_CacheFinish = m_Offset - m_CacheOffset;
            }
        }
        
        return ret;
    }
    
    protected int lockedForRead(byte[] bufOut, int start, int count) {
        int maxRead;		// Max. longitud de bytes a leer.
        int ret;		// Bytes leidos.
        int maxCopy;		// Max. longitud de bytes a copiar desde cache

        maxRead = Math.min(count, bufOut.length - start);
        ret = 0;
        while (ret < maxRead) {
            // Reclamar cache para leer ( maxRead - ret)
            maxCopy = seekCache(File.MODE_READ, maxRead - ret);
            // Del cache disponible, utilizar lo necesario.
            maxCopy = Math.min(maxCopy, maxRead - ret);
            // Si no hay que leer, hay un error o es fin de archivo.
            if (maxCopy == 0) {
                if (err()) {
                    ret = File.ERR;
                }
                break;
            }
            // Copiar desde cache
            Utils.memmove(bufOut, start + ret, // Destino
			  m_Cache, m_Offset - m_CacheOffset, // Origen
			  maxCopy);
            ret += maxCopy;
            // Mover el offset.
            m_Offset += maxCopy;
        }

        return ret;
    }
    
    protected int lockedForSeek(byte from, int count) {
        int ret;
        
        ret = File.ERR;
        // Asegurarnos que no hay nada en cache
        if (lockedForFlush() == 0) {
            // Reubicar Offset
            if (super.lockedForSeek( from, count) == 0) {
                // Si el nuevo Offset esta fuera del rango de Cache valido ...
                if ((m_Offset < m_CacheOffset) ||
		    (m_Offset > m_CacheOffset + m_CacheFinish)) {
                    // Considerar que nada esta en cache.
                    m_CacheOffset = m_Offset;
                    m_CacheFinish = 0;
                }
                ret = 0;
            }
        }
        
        return ret;
    }
    
    protected int lockedForFlush() {
        int modified;
        int ret;
        
        ret = 0;
        // Si hay bytes válidos y se ha modificado el Cache ...
        if ((m_CacheFinish > 0) && (m_CacheModify >= 0)) {
            ret = File.ERR;
            // Calcular la longitud de bytes modificados.
            modified = (m_Offset - m_CacheOffset) - m_CacheModify;
            // Ubicar el puntero en el archivo. Mucho cuidado!!!
            // Se debe usar super.lockedForSeek(), es decir  m_Offset = m_CacheOffset + m_CacheModify
            // De lo contrario ocurre recursividad.
            super.lockedForSeek(File.SEEK_START, m_CacheOffset + m_CacheModify);
            // Escribir 'modified' bytes en disco.
            ret = super.lockedForWrite(m_Cache, m_CacheModify, modified);
            // Si se escribieron algunos bytes ...
            if (ret > 0) {
                // ... ahora el  primier byte modificado es :
                m_CacheModify += ret;
                // Si se escribio todo ...
                if (ret == modified) {
                    // Ya no hay bytes modificados.
                    m_CacheModify = -1;
                    ret = 0;	// Se envió todo a disco.
                }
            }
        }
        
        return ret;
    }
    
    protected int lockedForClose() {
        int ret;
                
        ret = -1;
        if (lockedForFlush() == 0) {
            ret = super.lockedForClose();
        }
                
        return ret;
    }
    
    /**
     *  Establece la longitud en bytes utilizados por la memoria cache.
     *
     * @param length Longitud en bytes que se debe mantener en cache.
     */
    public boolean setCache(int length) {
        return (lock(FileStream.OPER_CACHE, null, (byte)0, null, 0, length) == 0);
    }
    
    public boolean flush() {
        return (lock(FileStream.OPER_FLUSH, null, (byte)0, null, 0, 0) == 0);
    }
    
    /**
     *  Intenta leer una linea completa del archivo y mueve el puntero.
     *
     * Lee bytes de un archivo y los almacena en un buffer hasta encontrar un fin
     * de linea o hasta llegar al final del archivo, lo que ocurra primero. 
     * Se considera fin de linea a la los bytes correspondientes a los utilizados
     * por las plataformas  Macintosh (\r), Unix (\n), o Windows (\r\n).
     *
     * @param bufOut Buffer que contiene la linea leida.
     * @param start Posición del primer byte correspondiente a la linea.
     *
     * @return Devuelve la longitud de la linea encontrada, 0 si se llega al final
     *         del archivo, -1 si ha ocurrido un error o el buffer no es sufiente
     *         para almacenar una linea. Utilice eof() para determinar si se ha
     *         llegado al fin del archivo o si la longitud de la linea es cero (0)
     *         y err() para determinar si el buffer no es suficiente para almacenar
     *         una linea o hay un error de entrada y salida.
     *
     */
    public int gets(byte [] bufOut, int start) {
        int ret;
        boolean macOS;
        int i;
        int r;

        ret = -1;
        macOS = false;
        i = 0;
	m_LineEnd = 0;
        while (i < bufOut.length - start) {
            
            // Leemos byte a byte desde la memoria cache. Si es <= 0 ...
            if (read(bufOut, start + i, 1) <= 0) {
                // ... es fin de archivo ...
                if (eof()) {
                    // ... devolvemos cero si no se ha leido ningun byte,
		    // o el número de bytes que forman la última línea
                    ret = (i == 0 ? 0 : i);
                }
                // En caso contraio retornamos -1 para inidicar error o
                // longitud del buffer insuficiente.
                break;
            }
            
            // Preguntamos si el byte agregado es \r o \n
            r = Utils.memmem(Utils.ENDOFLINE, 0, Utils.ENDOFLINE.length,
			     bufOut, start + i, 1);

            if (r == 0) {	// Si encontramos el \r ...
                // ... y ya era macOS
                if (macOS) {
                    // ... entonces es MacOS.
                    ret = i + 1;
                    // terminamos.
                    m_Offset--;
                    break;
                }
                // ... por el momento es Mac OS
                macOS = true;
		m_LineEnd++;
            } else if (r == 1) {	// Si encontramos el \n ...
                // ... o es Unix o es Windows
                ret = i + 1;
		m_LineEnd++;
                break;
            } else if (macOS) {	// Si se encuentra otro byte pero ya es Mac OS ...
                // ... definitivamente es MacOS.
                ret = i + 1;
                // Ignorar el último caracter leido.
                m_Offset--;
                break;
            }
            
            i++;
        }
        
        return ret;
    }
    
    /**
     *  Intenta leer una linea completa del archivo y mueve el puntero.
     *
     * A diferencia de gets() con dos parametros, esta funcion alamacena la linea
     * al princio del buffer.
     *
     * @param bufOut Buffer que contiene la linea leida.
     *
     * @return Devuelve la longitud de la linea encontrada, 0 si se llega al final
     *         del archivo, -1 si ha ocurrido un error o el buffer no es sufiente
     *         para almacenar una linea. Utilice eof() para determinar si se ha
     *         llegado al fin del archivo o si la longitud de la linea es cero (0)
     *         y err() para determinar si el buffer no es suficiente para almacenar
     *         una linea o hay un error de entrada y salida.
     */
    public int gets(byte [] bufOut) {
        return gets(bufOut, 0);
    }
    
    /** 
     *  Lee una linea completa y la almacena en m_Data.
     * 
     * Busca una linea completa, guarda sus bytes en m_Data y su longitud en
     * m_DataLen. Si la longitud de m_Data no es suficiente, el algoritmo
     * redimeciona el arreglo al doble de su longitud inicial.
     *
     * @return Devuelve la longitud de la linea encontrada, 0 si se llega al final del
     *         archivo, -1 si ha ocurrido un error. Utilice eof() para determinar si 
     *         a llegado al fin del archivo o si la longitud de la linea es cero (0),
     *         y err() para determinar error de entrada.
     *
     */
    private int getsLine() {
        byte[] tmp;
        int len;

	// La longitud de la linea es cero.
        m_DataLen = 0;
        // Intentar leer una linea
        len = gets(m_Data);
        // Si devuelve < 0 ...
        while (len < 0) {
            // ... probar que no sea error, ...
            if (err()) {
                // ... si es error, abandonamos!!!
                m_DataLen = 0;
                break;
            }
            // ... si no es error, continuar leyendo
            // Crear un nuevo buffer del doble de su tamanio inicial
            tmp = m_Data;
            m_Data = new byte[2*m_Data.length];
            // Copiar al nuevo buffer, lo hasta ahora leido
            m_DataLen = tmp.length;
            Utils.memcpy(m_Data, 0, tmp, 0, m_DataLen);
            tmp = null;		// Liberar memoria ;-)
            // Continuar leyendo el archivo
            len = gets(m_Data, m_DataLen);
        }
        m_DataLen += len;

	return m_DataLen;
    }

    /**
     *  Lee una linea completa del archivo y mueve el puntero.
     *
     * @return Devuelve una referencia a un nuevo objeto String que contiene la linea,
     *         null si la linea tiene longitud cero, si ha ocurrido un error, o se ha
     *         llegado al final del archivo. Utilice las funciones eof() y err() para
     *         determinar lo ocurrido.
     *
     */
    public String gets() {
        if (getsLine() > 0) {
	    return new String(m_Data, 0, m_DataLen);
	}
	return null;
    }
    
    /** 
     *  Lee todas las lineas de un archivo, incluyendo las lineas de longitud cero.
     *
     * La función se detiene si se llega al final del archivo, si hay un error de lectura
     * o la función FileGets.fileGetsLine() devuelve false. Utilce eof() o err() para 
     * determinar lo ocurrido.
     *
     * @param fg Referencia a una clase que implementa la interface FileGets.
     */
    public void gets( FileGets fg ) {
        while( !eof() && !err()) {
            if (getsLine() >= 0) {
		if(!fg.fileGetsLine(m_Data, m_DataLen, m_LineEnd )) {
		    break;
		}
	    }
        }
    }
    
    
    /**
     * Escribe una linea en un archivo.
     *
     * Esta función añade una cobinacion de caracteres CRLF
     *
     * @param bufIn Arreglo de bytes origen.
     * @param start Primer byte de bufIn que se debe escribir en el archivo.
     * @param count Número de bytes que se deben escribir en el archivo a partir de start.
     *
     * @return Si la función tiene éxito devuelve true, en caso de error devuelve false.
     */
    public boolean puts(final byte [] bufIn, int start, int count) {
        if (write(bufIn, start, count) == count) {
	    return (write(Utils.ENDOFLINE, 0, Utils.ENDOFLINE.length) == Utils.ENDOFLINE.length);
	}
	return false;
    }
    
    /**
     * Escribe una linea en un archivo.
     *
     * Esta función añade una cobinacion de caracteres CRLF
     *
     * @param bufIn Arreglo de bytes origen.
     * @param start Primer byte de bufIn que se debe escribir en el archivo.
     *
     * @return Si la función tiene éxito devuelve true, en caso de error devuelve false.
     */
    public boolean puts(final byte [] bufIn, int start) {
        return puts(bufIn, start, bufIn.length - start);
    }
    
    /**
     * Escribe una linea en un archivo.
     *
     * Esta función añade una cobinacion de caracteres CRLF
     *
     * @param bufIn Arreglo de bytes origen.
     *
     * @return Si la función tiene éxito devuelve true, en caso de error devuelve false.
     */
    public boolean puts(final byte [] bufIn) {
        return puts(bufIn, 0, bufIn.length);
    }
    
    /**
     * Escribe una linea en un archivo.
     *
     * Esta función añade una cobinacion de caracteres CRLF
     *
     * @param s Cadena de caracteres origen.
     *
     * @return Si la función tiene éxito devuelve true, en caso de error devuelve false.
     */
    public boolean puts(final String s) {
        return puts(s.getBytes());
    }
    
}
